date('c'),
'first_seen_url' => $sanitizeJourneyUrl($requestUri),
'first_referrer' => trim((string)($_SERVER['HTTP_REFERER'] ?? '')),
'user_agent' => trim((string)($_SERVER['HTTP_USER_AGENT'] ?? '')),
'accept_language' => trim((string)($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '')),
];
}
$_SESSION['visitor_meta']['last_seen_at'] = date('c');
$_SESSION['visitor_meta']['last_seen_url'] = $sanitizeJourneyUrl($requestUri);
if (in_array($requestMethod, ['GET', 'HEAD'], true)) {
$normalizedPath = strtolower((string)$path);
$noisePaths = [
'favicon.ico',
'robots.txt',
'manifest.json',
'site.webmanifest',
'browserconfig.xml',
'apple-touch-icon.png',
'apple-touch-icon-precomposed.png',
];
$excluded = in_array($normalizedPath, $noisePaths, true)
|| ($path !== '' && preg_match('/^(admin|login|logout|media)(\/|$)/i', $path) === 1);
$existingJourney = $_SESSION['visitor_journey'] ?? [];
if (is_array($existingJourney) && !empty($existingJourney)) {
$existingJourney = array_values(array_filter($existingJourney, static function($item) use ($noisePaths) {
$u = trim((string)($item['u'] ?? ''));
if ($u === '') {
return false;
}
$urlPath = strtolower((string)(parse_url($u, PHP_URL_PATH) ?? ''));
$urlPath = ltrim($urlPath, '/');
return $urlPath === '' || !in_array($urlPath, $noisePaths, true);
}));
$_SESSION['visitor_journey'] = $existingJourney;
}
if (!$excluded) {
$journey = $_SESSION['visitor_journey'] ?? [];
if (!is_array($journey)) {
$journey = [];
}
$entry = ['t' => date('c'), 'u' => $sanitizeJourneyUrl($requestUri)];
$last = !empty($journey) ? $journey[count($journey) - 1] : null;
if (empty($last) || ($last['u'] ?? '') !== $entry['u']) {
$journey[] = $entry;
if (count($journey) > 25) {
$journey = array_slice($journey, -25);
}
$_SESSION['visitor_journey'] = $journey;
}
}
}
// Token access must be applied before visibility is calculated.
if (!$security->isLoggedIn()) {
$token = $_GET['token'] ?? $_GET['audience_token'] ?? $_GET['guest_token'] ?? $_GET['family_token'] ?? null;
if ($token) {
$security->setAudienceAccess($token);
}
}
// Access visibility scope
$visibleAccessLevels = getVisibleAccessLevels($security);
$audienceSegmentFilter = getAudienceSegmentFilter($security);
$renderNotFoundResponse = function() use ($db, $post, $template, $media, $security, $requestAccessToken, $visibleAccessLevels, $audienceSegmentFilter) {
http_response_code(404);
$page404 = null;
$page404Id = (int)$db->getSetting('system_page_404_id', '0');
if ($page404Id > 0) {
$assigned404Page = $post->getById($page404Id);
$assigned404Path = trim((string)($assigned404Page['full_path'] ?? ''));
if ($assigned404Page && ($assigned404Page['post_type'] ?? '') === 'page' && ($assigned404Page['status'] ?? '') === 'published' && $assigned404Path !== '') {
$page404 = $post->getByPath($assigned404Path, $visibleAccessLevels, $audienceSegmentFilter);
}
}
if ($page404 && canAccessTokenProtected($page404, $requestAccessToken, $security->isLoggedIn())) {
$page404Template = resolveContentTemplateName($page404, false);
echo $template->render($page404Template, [
'entry' => $page404,
'page' => $page404,
'post' => $page404,
'section' => [
'name' => $page404['title'],
'slug' => $page404['slug'],
'description' => '',
'content' => $page404['content'],
'full_path' => $page404['full_path'] ?? '',
],
'children' => [],
'posts' => [],
'has_children' => false,
'media' => $media,
'post_media' => !empty($page404['id']) ? $media->getByPost((int)$page404['id']) : [],
'page_title' => $page404['title'],
'meta_source' => $page404,
'csrf_token' => $security->generateCSRFToken()
]);
return;
}
echo $template->render('404', ['page_title' => 'Page Not Found']);
};
$renderPageResponse = function($pageData, $options = []) use ($post, $template, $security, $requestAccessToken, $visibleAccessLevels, $audienceSegmentFilter, $renderNotFoundResponse) {
if (!$pageData) {
return false;
}
if (!canAccessTokenProtected($pageData, $requestAccessToken, $security->isLoggedIn())) {
$renderNotFoundResponse();
return true;
}
$children = $post->getChildrenByParent($pageData['id'], $visibleAccessLevels, 100, 0, $audienceSegmentFilter);
if (!empty($children)) {
$templateName = resolveHierarchyTemplateName($pageData);
echo $template->render($templateName, ['section' => ['name' => $pageData['title'], 'slug' => $pageData['slug'], 'description' => '', 'content' => $pageData['content'], 'full_path' => $pageData['full_path'] ?? ''], 'meta_source' => $pageData, 'posts' => $children, 'page_title' => $pageData['title']]);
} else {
$templateName = $options['template_name'] ?? resolveContentTemplateName($pageData, false);
$templateVars = [
'entry' => $pageData,
'page' => $pageData,
'post' => $pageData,
'children' => [],
'posts' => [],
'has_children' => false,
'page_title' => $pageData['title'],
'meta_source' => $pageData,
'csrf_token' => $security->generateCSRFToken(),
];
if (!empty($options['template_vars']) && is_array($options['template_vars'])) {
$templateVars = array_merge($templateVars, $options['template_vars']);
}
echo $template->render($templateName, $templateVars);
}
return true;
};
// Simple routing
if (empty($path)) {
$homePage = null;
$homePageId = (int)$db->getSetting('system_page_home_id', '0');
if ($homePageId > 0) {
$assignedHomePage = $post->getById($homePageId);
$assignedHomePath = trim((string)($assignedHomePage['full_path'] ?? ''));
if ($assignedHomePage && ($assignedHomePage['post_type'] ?? '') === 'page' && ($assignedHomePage['status'] ?? '') === 'published' && $assignedHomePath !== '') {
$homePage = $post->getByPath($assignedHomePath, $visibleAccessLevels, $audienceSegmentFilter);
}
}
if (!empty($homePage)) {
echo $template->render('home', [
'page_title' => 'Home',
'home_content' => $homePage,
'meta_source' => $homePage,
'csrf_token' => $security->generateCSRFToken(),
]);
} else {
$homeContentPost = $post->getBySlugAndType('index', 'page', $visibleAccessLevels, $audienceSegmentFilter);
if (!empty($homeContentPost)) {
echo $template->render('home', [
'page_title' => 'Home',
'home_content' => $homeContentPost,
'meta_source' => $homeContentPost,
'csrf_token' => $security->generateCSRFToken(),
]);
} else {
$posts = $post->getAll($visibleAccessLevels, null, 20, 0, $audienceSegmentFilter);
echo $template->render('home', [
'posts' => $posts,
'page_title' => 'Home',
]);
}
}
} elseif ($path === 'login') {
// Login page
if ($security->isLoggedIn()) {
redirect('/admin');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) {
$error = 'Invalid security token. Please refresh the page and try again.';
} else {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$result = $security->login($username, $password);
if ($result['success']) {
redirect('/admin');
} else {
$error = $result['error'];
}
}
}
echo $template->render('admin/login', [
'page_title' => 'Login',
'layout_mode' => 'admin',
'error' => $error ?? null,
'csrf_token' => $security->generateCSRFToken()
]);
} elseif ($path === 'logout' || $path === 'admin/logout') {
// Logout
$security->logout();
redirect('/');
} elseif ($path === 'audience-access' || $path === 'guest-access' || $path === 'family-access') {
// Audience access (legacy routes are still supported)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) {
$error = 'Invalid security token. Please refresh the page and try again.';
} else {
$key = $_POST['key'] ?? '';
if ($security->setAudienceAccess($key)) {
redirect('/');
} else {
$error = 'Invalid access key';
}
}
}
echo $template->render('audience-access', [
'page_title' => 'Audience Access',
'error' => $error ?? null,
'csrf_token' => $security->generateCSRFToken()
]);
} elseif (preg_match('#^uploads/cache/(\d+)/(\d+)w\.(webp|jpg)$#', $path, $matches)) {
$mediaId = (int)$matches[1];
$width = (int)$matches[2];
$format = $matches[3];
$mediaRow = $media->getById($mediaId);
if (!$mediaRow || (int)$mediaRow['access_level'] !== 0) {
http_response_code(404);
exit;
}
$variant = $media->renderVariant($mediaId, $width, $format, $visibleAccessLevels);
if (!$variant) {
http_response_code(404);
exit;
}
header('Content-Type: ' . $variant['mime']);
header('Cache-Control: public, max-age=31536000, immutable');
readfile($variant['path']);
exit;
} elseif (preg_match('#^secure-media/original/(\d+)$#', $path, $matches)) {
$mediaId = (int)$matches[1];
$mediaRow = $media->getById($mediaId);
if (!$mediaRow) {
http_response_code(404);
exit;
}
if ((int)$mediaRow['access_level'] === 0) {
redirect($media->getOriginalUrl($mediaRow), 302);
}
$original = $media->renderOriginal($mediaId, $visibleAccessLevels);
if (!$original) {
http_response_code(404);
exit;
}
header('Content-Type: ' . $original['mime']);
$safeName = str_replace('"', '', (string)($original['name'] ?? 'file'));
header('Content-Disposition: inline; filename="' . $safeName . '"');
header('Cache-Control: private, max-age=0, no-store');
readfile($original['path']);
exit;
} elseif (preg_match('#^media/([a-z0-9\-]{1,160})\.([a-z0-9]{2,5})$#i', $path, $matches)) {
$slug = strtolower((string)$matches[1]);
$requestedExt = strtolower((string)$matches[2]);
$mediaRow = $media->getByPublicSlug($slug);
if (!$mediaRow) {
http_response_code(404);
exit;
}
$qs = (string)($_SERVER['QUERY_STRING'] ?? '');
$widthRequest = 0;
if (isset($_GET['w'])) {
$widthRequest = (int)$_GET['w'];
} elseif (isset($_GET['width'])) {
$widthRequest = (int)$_GET['width'];
} elseif (preg_match('/(\d+)w?/i', $qs, $m)) {
$widthRequest = (int)$m[1];
}
$fmt = (string)($_GET['fmt'] ?? $_GET['format'] ?? '');
$fmt = strtolower(trim($fmt));
if ($fmt === 'jpeg') {
$fmt = 'jpg';
}
if (!in_array($fmt, ['webp', 'jpg'], true)) {
$fmt = '';
}
$storedExt = strtolower((string)pathinfo((string)$mediaRow['file_path'], PATHINFO_EXTENSION));
if ($storedExt === 'jpeg') {
$storedExt = 'jpg';
}
if ($requestedExt === 'jpeg') {
$requestedExt = 'jpg';
}
if ($storedExt !== '' && $requestedExt !== $storedExt && $fmt === '') {
$target = '/media/' . rawurlencode($slug) . '.' . $storedExt;
if ($qs !== '') {
$target .= '?' . $qs;
}
redirect($target, 301);
}
$isPublic = (int)($mediaRow['access_level'] ?? 0) === 0;
if ($widthRequest > 0) {
$allowed = $media->getAllowedWidths();
$allowed = array_map('intval', array_values($allowed ?: []));
sort($allowed);
$width = 0;
foreach ($allowed as $w) {
if ($w >= $widthRequest) {
$width = $w;
break;
}
}
if ($width <= 0) {
$width = (int)end($allowed);
}
$format = $fmt !== '' ? $fmt : 'webp';
$variant = $media->renderVariant((int)$mediaRow['id'], $width, $format, $visibleAccessLevels);
if (!$variant) {
http_response_code(404);
exit;
}
header('Content-Type: ' . $variant['mime']);
header('Cache-Control: ' . ($isPublic ? 'public, max-age=86400' : 'private, max-age=0, no-store'));
readfile($variant['path']);
exit;
}
if (!$isPublic) {
$original = $media->renderOriginal((int)$mediaRow['id'], $visibleAccessLevels);
if (!$original) {
http_response_code(404);
exit;
}
header('Content-Type: ' . $original['mime']);
$safeName = str_replace('"', '', (string)($original['name'] ?? 'file'));
header('Content-Disposition: inline; filename="' . $safeName . '"');
header('Cache-Control: private, max-age=0, no-store');
readfile($original['path']);
exit;
}
if (!is_file((string)($mediaRow['file_path'] ?? ''))) {
http_response_code(404);
exit;
}
header('Content-Type: ' . ((string)($mediaRow['mime_type'] ?? 'application/octet-stream')));
header('Cache-Control: public, max-age=86400');
readfile($mediaRow['file_path']);
exit;
} elseif (preg_match('#^secure-media/(\d+)/(\d+)w\.(webp|jpg)$#', $path, $matches)) {
$mediaId = (int)$matches[1];
$width = (int)$matches[2];
$format = $matches[3];
$variant = $media->renderVariant($mediaId, $width, $format, $visibleAccessLevels);
if (!$variant) {
http_response_code(404);
exit;
}
header('Content-Type: ' . $variant['mime']);
header('Cache-Control: private, max-age=0, no-store');
readfile($variant['path']);
exit;
} elseif ($path === 'admin') {
if (!$security->isLoggedIn()) {
redirect('/login');
}
$dashboardStats = [
'total_content' => (int)($db->queryOne('SELECT COUNT(*) AS count FROM posts')['count'] ?? 0),
'published_items' => (int)($db->queryOne('SELECT COUNT(*) AS count FROM posts WHERE status = ?', ['published'])['count'] ?? 0),
'draft_items' => (int)($db->queryOne('SELECT COUNT(*) AS count FROM posts WHERE status = ?', ['draft'])['count'] ?? 0),
'pages_projects' => (int)($db->queryOne('SELECT COUNT(*) AS count FROM posts WHERE post_type IN ("page", "project")')['count'] ?? 0),
'blog_posts' => (int)($db->queryOne('SELECT COUNT(*) AS count FROM posts WHERE post_type = ?', ['post'])['count'] ?? 0),
'audience_segments' => (int)($db->queryOne('SELECT COUNT(*) AS count FROM guest_segments')['count'] ?? 0),
'taxonomy_terms' => (int)($db->queryOne('SELECT COUNT(*) AS count FROM terms')['count'] ?? 0),
];
$recentContent = $db->query(
'SELECT id, title, slug, full_path, post_type, status, created_at, updated_at
FROM posts
ORDER BY COALESCE(updated_at, created_at) DESC, id DESC
LIMIT 8'
) ?: [];
echo $template->render('admin/dashboard', [
'page_title' => 'Dashboard',
'layout_mode' => 'admin',
'dashboard_stats' => $dashboardStats,
'recent_content' => $recentContent,
]);
} elseif ($path === 'settings' || $path === 'admin/settings') {
// Settings page
if (!$security->isLoggedIn()) {
redirect('/login');
}
if ($path === 'settings' && $_SERVER['REQUEST_METHOD'] === 'GET') {
redirect('/admin/settings', 301);
}
// Get success/error messages from URL
$success = null;
$error = null;
if (isset($_GET['success'])) {
switch ($_GET['success']) {
case 'guest_key_updated':
case 'family_key_updated':
case 'audience_key_updated':
$success = 'Audience access key updated successfully!';
break;
case 'password_updated':
$success = 'Password changed successfully!';
break;
case 'theme_updated':
$success = 'Admin theme updated successfully!';
break;
case 'frontend_theme_updated':
$success = 'Frontend theme updated successfully!';
break;
case 'system_pages_updated':
$success = 'System pages updated successfully!';
break;
case 'updates_applied':
$success = 'Pending updates applied successfully!';
break;
}
}
if (isset($_GET['error'])) {
$error = $_GET['error'];
if ($error === 'invalid_token') {
$error = 'Invalid security token. Please try again.';
}
}
$systemPageOptions = array_values(array_filter($post->getAllPages() ?: [], function($item) {
return ($item['post_type'] ?? '') === 'page';
}));
$systemPageSettings = [
'home' => (int)$db->getSetting('system_page_home_id', '0'),
'blog_index' => (int)$db->getSetting('system_page_blog_index_id', '0'),
'contact' => (int)$db->getSetting('system_page_contact_id', '0'),
'404' => (int)$db->getSetting('system_page_404_id', '0'),
];
$migrationStatus = $migrationRunner->getStatus();
$pendingMigrations = $migrationRunner->getPendingMigrations();
$mediaVariantWidths = $db->getSetting('media_variant_widths') ?: '320,480,640,768,1024,1280,1600,1920,2560';
$frontendTheme = getCurrentFrontendTheme();
$frontendThemeOptions = getAvailableFrontendThemes();
echo $template->render('admin/settings', [
'page_title' => 'Settings',
'layout_mode' => 'admin',
'csrf_token' => $security->generateCSRFToken(),
'success' => $success,
'error' => $error,
'system_page_options' => $systemPageOptions,
'system_page_settings' => $systemPageSettings,
'migration_status' => $migrationStatus,
'pending_migrations' => $pendingMigrations,
'media_variant_widths' => $mediaVariantWidths,
'frontend_theme' => $frontendTheme,
'frontend_theme_options' => $frontendThemeOptions,
]);
} elseif ($path === 'settings/update-theme' || $path === 'admin/settings/update-theme') {
if (!$security->isLoggedIn()) {
redirect('/login');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) {
redirect('/admin/settings?error=invalid_token');
}
$adminThemeMode = trim((string)($_POST['admin_theme_mode'] ?? 'light'));
$allowedModes = ['light', 'dark'];
if (!in_array($adminThemeMode, $allowedModes, true)) {
redirect('/admin/settings?error=' . urlencode('Invalid admin theme mode'));
}
if (!$db->setSetting('admin_theme_mode', $adminThemeMode)) {
redirect('/admin/settings?error=' . urlencode('Failed to save admin theme'));
}
$template->set('admin_theme_mode', $adminThemeMode);
redirect('/admin/settings?success=theme_updated');
}
redirect('/admin/settings');
} elseif ($path === 'settings/update-frontend-theme' || $path === 'admin/settings/update-frontend-theme') {
if (!$security->isLoggedIn()) {
redirect('/login');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) {
redirect('/admin/settings?error=invalid_token');
}
$frontendTheme = trim((string)($_POST['frontend_theme'] ?? 'default'));
$allowedThemes = getAvailableFrontendThemes();
if (!isset($allowedThemes[$frontendTheme])) {
redirect('/admin/settings?error=' . urlencode('Invalid frontend theme selected'));
}
if (!$db->setSetting('frontend_theme', $frontendTheme)) {
redirect('/admin/settings?error=' . urlencode('Failed to save frontend theme'));
}
redirect('/admin/settings?success=frontend_theme_updated');
}
redirect('/admin/settings');
} elseif ($path === 'settings/update-media-settings' || $path === 'admin/settings/update-media-settings') {
if (!$security->isLoggedIn()) {
redirect('/login');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) {
redirect('/admin/settings?error=invalid_token');
}
$variantWidths = trim((string)($_POST['media_variant_widths'] ?? ''));
if ($variantWidths === '') {
redirect('/admin/settings?error=' . urlencode('Image variant widths cannot be empty.'));
}
// Validate: only digits, commas, and spaces
$validated = preg_replace('/[^0-9,\s]/', '', $variantWidths);
$widths = array_filter(array_map('intval', array_map('trim', explode(',', $validated))));
if (empty($widths)) {
redirect('/admin/settings?error=' . urlencode('Invalid image variant widths format. Use comma-separated numbers.'));
}
// Sort and deduplicate
$widths = array_values(array_unique($widths));
sort($widths);
$widthsString = implode(',', $widths);
if (!$db->setSetting('media_variant_widths', $widthsString)) {
redirect('/admin/settings?error=' . urlencode('Failed to save media settings.'));
}
redirect('/admin/settings?success=media_settings_updated');
}
redirect('/admin/settings');
} elseif ($path === 'settings/run-updates' || $path === 'admin/settings/run-updates') {
if (!$security->isLoggedIn()) {
redirect('/login');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) {
redirect('/admin/settings?error=invalid_token');
}
if ($migrationRunner->runPending()) {
redirect('/admin/settings?success=updates_applied');
}
redirect('/admin/settings?error=' . urlencode($migrationRunner->getLastError() ?: 'Failed to apply pending updates.'));
}
redirect('/admin/settings');
} elseif ($path === 'settings/update-system-pages' || $path === 'admin/settings/update-system-pages') {
if (!$security->isLoggedIn()) {
redirect('/login');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) {
redirect('/admin/settings?error=invalid_token');
}
foreach (['home' => 'system_page_home_id', 'blog_index' => 'system_page_blog_index_id', 'contact' => 'system_page_contact_id', '404' => 'system_page_404_id'] as $inputKey => $settingKey) {
$pageId = (int)($_POST[$inputKey . '_page_id'] ?? 0);
if ($pageId > 0) {
$pageItem = $post->getById($pageId);
if (!$pageItem || ($pageItem['post_type'] ?? '') !== 'page' || ($pageItem['status'] ?? '') !== 'published') {
redirect('/admin/settings?error=' . urlencode('Invalid system page selection.'));
}
}
if (!$db->setSetting($settingKey, (string)$pageId)) {
redirect('/admin/settings?error=' . urlencode('Failed to save system pages.'));
}
}
redirect('/admin/settings?success=system_pages_updated');
}
redirect('/admin/settings');
} elseif ($path === 'settings/update-password' || $path === 'admin/settings/update-password') {
// Update password
if (!$security->isLoggedIn()) {
redirect('/login');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) {
redirect('/admin/settings?error=invalid_token');
}
$currentPassword = $_POST['current_password'] ?? '';
$newPassword = $_POST['new_password'] ?? '';
$confirmPassword = $_POST['confirm_password'] ?? '';
// Check if passwords match
if ($newPassword !== $confirmPassword) {
redirect('/admin/settings?error=' . urlencode('Passwords do not match'));
}
$userId = $security->getCurrentUser()['id'];
$result = $security->changePassword($userId, $currentPassword, $newPassword);
if ($result['success']) {
redirect('/admin/settings?success=password_updated');
} else {
redirect('/admin/settings?error=' . urlencode($result['error']));
}
}
redirect('/admin/settings');
} elseif ($path === 'audiences' || $path === 'admin/audiences') {
if (!$security->isLoggedIn()) {
redirect('/login');
}
if ($path === 'audiences' && $_SERVER['REQUEST_METHOD'] === 'GET') {
redirect('/admin/audiences', 301);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) {
redirect('/admin/audiences?error=invalid_token');
}
$action = $_POST['action'] ?? '';
if ($action === 'create-segment') {
$segmentName = $_POST['segment_name'] ?? '';
$segmentDescription = $_POST['segment_description'] ?? '';
$includePublicContent = !empty($_POST['include_public_content']) ? 1 : 0;
if ($post->createAudienceSegment($segmentName, $segmentDescription, $includePublicContent)) {
redirect('/admin/audiences?success=segment_created');
}
redirect('/admin/audiences?error=' . urlencode($post->getLastError() ?: 'segment_create_failed'));
}
if ($action === 'update-segment-visibility') {
$segmentId = (int)($_POST['segment_id'] ?? 0);
$includePublicContent = !empty($_POST['include_public_content']) ? 1 : 0;
if ($post->updateAudienceSegmentVisibility($segmentId, $includePublicContent)) {
redirect('/admin/audiences?success=segment_updated');
}
redirect('/admin/audiences?error=' . urlencode($post->getLastError() ?: 'segment_update_failed'));
}
if ($action === 'delete-segment') {
$segmentId = (int)($_POST['segment_id'] ?? 0);
if ($post->deleteAudienceSegment($segmentId)) {
redirect('/admin/audiences?success=segment_deleted');
}
redirect('/admin/audiences?error=' . urlencode($post->getLastError() ?: 'segment_delete_failed'));
}
if ($action === 'regenerate-token') {
$segmentId = (int)($_POST['segment_id'] ?? 0);
$tokenData = $security->regenerateAudienceSegmentToken($segmentId, 6);
if ($tokenData) {
redirect('/admin/audiences?success=token_regenerated');
}
redirect('/admin/audiences?error=' . urlencode('token_regenerate_failed'));
}
}
$segments = $post->getAudienceSegments() ?: [];
$tokens = $security->getAudienceTokens() ?: [];
$tokensBySegmentId = [];
foreach ($tokens as $tokenRow) {
$segmentId = (int)($tokenRow['segment_id'] ?? 0);
if ($segmentId > 0 && !isset($tokensBySegmentId[$segmentId]) && (int)($tokenRow['is_active'] ?? 0) === 1) {
$tokensBySegmentId[$segmentId] = $tokenRow;
}
}
foreach ($segments as &$segment) {
$segmentId = (int)($segment['id'] ?? 0);
if ($segmentId <= 0) {
continue;
}
$tokenRow = $tokensBySegmentId[$segmentId] ?? $security->ensureAudienceSegmentToken($segmentId, 6);
$segment['token'] = $tokenRow['token'] ?? '';
$segment['share_link'] = rtrim($config['site']['url'], '/') . '?token=' . rawurlencode($segment['token']);
}
unset($segment);
echo $template->render('admin/audiences', [
'page_title' => 'Audience Segments',
'layout_mode' => 'admin',
'segments' => $segments,
'success' => $_GET['success'] ?? null,
'error' => $_GET['error'] ?? null,
'csrf_token' => $security->generateCSRFToken()
]);
} elseif ($path === 'taxonomies' || $path === 'admin/taxonomies') {
if (!$security->isLoggedIn()) {
redirect('/login');
}
if ($path === 'taxonomies' && $_SERVER['REQUEST_METHOD'] === 'GET') {
redirect('/admin/taxonomies', 301);
}
$activeTaxonomy = $_GET['taxonomy'] ?? 'category';
$taxonomyMap = [];
foreach (($post->getTaxonomies() ?? []) as $taxonomyItem) {
$taxonomyMap[$taxonomyItem['slug']] = $taxonomyItem;
}
if (!isset($taxonomyMap[$activeTaxonomy])) {
$activeTaxonomy = array_key_first($taxonomyMap) ?: 'category';
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) {
redirect('/admin/taxonomies?taxonomy=' . urlencode($activeTaxonomy) . '&error=invalid_token');
}
$action = $_POST['action'] ?? '';
if ($action === 'create-term') {
$taxonomySlug = $_POST['taxonomy_slug'] ?? $activeTaxonomy;
$termName = $_POST['term_name'] ?? '';
$termDescription = $_POST['term_description'] ?? '';
if ($post->createTerm($taxonomySlug, $termName, $termDescription)) {
redirect('/admin/taxonomies?taxonomy=' . urlencode($taxonomySlug) . '&success=term_created');
}
redirect('/admin/taxonomies?taxonomy=' . urlencode($taxonomySlug) . '&error=' . urlencode($post->getLastError() ?: 'term_create_failed'));
}
if ($action === 'delete-term') {
$taxonomySlug = $_POST['taxonomy_slug'] ?? $activeTaxonomy;
$termId = (int)($_POST['term_id'] ?? 0);
if ($post->deleteTerm($termId)) {
redirect('/admin/taxonomies?taxonomy=' . urlencode($taxonomySlug) . '&success=term_deleted');
}
redirect('/admin/taxonomies?taxonomy=' . urlencode($taxonomySlug) . '&error=' . urlencode($post->getLastError() ?: 'term_delete_failed'));
}
}
echo $template->render('admin/taxonomies', [
'page_title' => 'Taxonomies',
'layout_mode' => 'admin',
'taxonomies' => array_values($taxonomyMap),
'active_taxonomy' => $activeTaxonomy,
'terms' => $post->getTermsWithUsage($activeTaxonomy),
'success' => $_GET['success'] ?? null,
'error' => $_GET['error'] ?? null,
'csrf_token' => $security->generateCSRFToken()
]);
} elseif ($path === 'admin/media-inline-upload') {
header('Content-Type: application/json; charset=utf-8');
register_shutdown_function(static function () {
$err = error_get_last();
if ($err && in_array((int)($err['type'] ?? 0), [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR], true)) {
if (!headers_sent()) {
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
}
echo json_encode([
'success' => false,
'error' => 'php_fatal',
'message' => (string)($err['message'] ?? ''),
'file' => (string)($err['file'] ?? ''),
'line' => (int)($err['line'] ?? 0),
]);
}
});
try {
if (!$security->isLoggedIn()) {
jsonResponse(['success' => false, 'error' => 'unauthorized'], 401);
}
if (strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) !== 'POST') {
jsonResponse(['success' => false, 'error' => 'method_not_allowed'], 405);
}
if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) {
jsonResponse(['success' => false, 'error' => 'invalid_token'], 400);
}
$userId = (int)($security->getCurrentUser()['id'] ?? 0);
$postId = (int)($_POST['post_id'] ?? 0);
if ($postId <= 0) {
jsonResponse(['success' => false, 'error' => 'invalid_post'], 400);
}
$postRow = $post->getById($postId);
if (!$postRow || (int)($postRow['user_id'] ?? 0) !== $userId) {
jsonResponse(['success' => false, 'error' => 'forbidden'], 403);
}
$file = $_FILES['file'] ?? null;
if (!$file || !isset($file['tmp_name'])) {
jsonResponse(['success' => false, 'error' => 'no_file'], 400);
}
$result = $media->upload($file, [
'post_id' => $postId,
'created_by' => $userId,
'title' => (string)($_POST['title'] ?? ''),
'alt_text' => (string)($_POST['alt_text'] ?? ''),
'caption' => (string)($_POST['caption'] ?? ''),
'public_slug' => (string)($_POST['public_slug'] ?? ''),
'access_level' => 0,
]);
if (empty($result['success'])) {
jsonResponse(['success' => false, 'error' => $result['error'] ?? 'upload_failed'], 400);
}
$mediaId = (int)($result['id'] ?? 0);
$mediaRow = $media->getById($mediaId);
if (!$mediaRow) {
jsonResponse(['success' => false, 'error' => 'media_not_found'], 400);
}
$ext = strtolower((string)pathinfo((string)($mediaRow['file_path'] ?? ''), PATHINFO_EXTENSION));
if ($ext === 'jpeg') {
$ext = 'jpg';
}
$publicSlug = trim((string)($mediaRow['public_slug'] ?? ''));
$publicUrlBase = $publicSlug !== ''
? url('/media/' . $publicSlug . '.' . ($ext !== '' ? $ext : 'jpg'))
: $media->getOriginalUrl($mediaRow);
$alt = trim((string)($mediaRow['alt_text'] ?? ''));
$title = trim((string)($mediaRow['title'] ?? ''));
$allowed = $media->getAllowedWidths();
$allowed = array_map('intval', array_values($allowed ?: []));
sort($allowed);
$originalWidth = (int)($mediaRow['width'] ?? 0);
$srcsetWebp = [];
$srcsetJpg = [];
foreach ($allowed as $w) {
if ($originalWidth > 0 && $w > $originalWidth + 50) {
continue;
}
$srcsetWebp[] = $publicUrlBase . '?' . $w . 'w&fmt=webp ' . $w . 'w';
$srcsetJpg[] = $publicUrlBase . '?' . $w . 'w&fmt=jpg ' . $w . 'w';
}
$defaultW = 0;
foreach ($allowed as $w) {
if ($w >= 640) {
$defaultW = $w;
break;
}
}
if ($defaultW <= 0) {
$defaultW = (int)end($allowed);
}
$srcJpg = $publicUrlBase . '?' . $defaultW . 'w&fmt=jpg';
$dimAttrs = '';
if (!empty($mediaRow['width']) && !empty($mediaRow['height'])) {
$dimAttrs = ' width="' . (int)$mediaRow['width'] . '" height="' . (int)$mediaRow['height'] . '"';
}
if ($title !== '') {
$dimAttrs .= ' title="' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '"';
}
$embedHtml = ''
. ''
. '
'
. '';
$simpleImgAttrs = '';
if (!empty($mediaRow['width']) && !empty($mediaRow['height'])) {
$simpleImgAttrs .= ' width="' . (int)$mediaRow['width'] . '" height="' . (int)$mediaRow['height'] . '"';
}
if ($title !== '') {
$simpleImgAttrs .= ' title="' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '"';
}
$simpleImg = '
';
$inlinePostUpdate = [];
if (!empty($_POST['set_featured'])) {
$inlinePostUpdate['featured_media_id'] = $mediaId;
}
if (!empty($_POST['set_og_image'])) {
$inlinePostUpdate['og_media_id'] = $mediaId;
}
if (!empty($inlinePostUpdate)) {
$post->update($postId, $inlinePostUpdate, $userId);
}
jsonResponse([
'success' => true,
'media_id' => $mediaId,
'media_title' => (string)($mediaRow['title'] ?? $mediaRow['original_name'] ?? ''),
'public_slug' => $publicSlug,
'public_url' => $publicUrlBase,
'embed_html' => $embedHtml,
'simple_img' => $simpleImg,
], 200);
} catch (\Throwable $e) {
error_log('[media-inline-upload] ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
if (!headers_sent()) {
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
}
echo json_encode([
'success' => false,
'error' => 'php_throw',
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
}
} elseif ($path === 'admin/media') {
if (!$security->isLoggedIn()) {
redirect('/login');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$returnTo = (string)($_POST['return_to'] ?? '');
if ($returnTo === '' || strpos($returnTo, '/') !== 0 || strpos($returnTo, '/admin') !== 0 || preg_match('/[\r\n]/', $returnTo)) {
$returnTo = '/admin/media';
}
$appendParam = static function($url, $key, $value) {
$sep = (strpos($url, '?') === false) ? '?' : '&';
return $url . $sep . rawurlencode((string)$key) . '=' . rawurlencode((string)$value);
};
if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) {
redirect($appendParam($returnTo, 'error', 'invalid_token'));
}
$action = $_POST['action'] ?? '';
if ($action === 'attach') {
$mediaId = (int)($_POST['media_id'] ?? 0);
$postId = (int)($_POST['post_id'] ?? 0);
if ($mediaId > 0 && $postId > 0 && $media->attachToPost($mediaId, $postId)) {
redirect($appendParam($returnTo, 'success', 'attached'));
}
redirect($appendParam($returnTo, 'error', 'attach_failed'));
}
if ($action === 'detach') {
$mediaId = (int)($_POST['media_id'] ?? 0);
if ($mediaId > 0 && $media->detachFromPost($mediaId)) {
redirect($appendParam($returnTo, 'success', 'detached'));
}
redirect($appendParam($returnTo, 'error', 'detach_failed'));
}
if ($action === 'update') {
$mediaId = (int)($_POST['media_id'] ?? 0);
if ($mediaId <= 0) {
redirect($appendParam($returnTo, 'error', 'invalid_media'));
}
$currentMedia = $media->getById($mediaId);
$oldPostId = (int)($currentMedia['post_id'] ?? 0);
$data = [];
foreach (['alt_text', 'title', 'caption', 'tags', 'galleries', 'public_slug'] as $field) {
if (array_key_exists($field, $_POST)) {
$data[$field] = $_POST[$field];
}
}
if (array_key_exists('access_level', $_POST)) {
$data['access_level'] = (int)$_POST['access_level'];
}
$postId = (int)($_POST['post_id'] ?? 0);
if ($postId > 0) {
$data['post_id'] = $postId;
} else {
$data['post_id'] = null;
}
if ($media->update($mediaId, $data)) {
if (array_key_exists('post_id', $data)) {
$newPostId = (int)$data['post_id'];
if ($newPostId !== $oldPostId) {
$media->attachToPost($mediaId, $newPostId);
}
}
redirect($appendParam($returnTo, 'success', 'updated'));
}
redirect($appendParam($returnTo, 'error', $media->getLastError() ?: 'update_failed'));
}
if ($action === 'upload') {
$accessLevel = (int)($_POST['access_level'] ?? 0);
if (!in_array($accessLevel, [0, 1, 2], true)) {
$accessLevel = 0;
}
$caption = (string)($_POST['caption'] ?? '');
$tags = $_POST['tags'] ?? '';
$galleries = $_POST['galleries'] ?? '';
$userId = (int)($security->getCurrentUser()['id'] ?? 0);
$postId = (int)($_POST['post_id'] ?? 0);
$files = $_FILES['files'] ?? null;
if (!$files || !isset($files['name'])) {
redirect($appendParam($returnTo, 'error', 'no_files'));
}
$uploaded = 0;
$firstError = null;
$count = is_array($files['name']) ? count($files['name']) : 1;
for ($i = 0; $i < $count; $i++) {
$file = [
'name' => is_array($files['name']) ? $files['name'][$i] : $files['name'],
'type' => is_array($files['type']) ? $files['type'][$i] : $files['type'],
'tmp_name' => is_array($files['tmp_name']) ? $files['tmp_name'][$i] : $files['tmp_name'],
'error' => is_array($files['error']) ? $files['error'][$i] : $files['error'],
'size' => is_array($files['size']) ? $files['size'][$i] : $files['size'],
];
if (($file['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) {
$firstError = $firstError ?: 'Upload error.';
continue;
}
$options = [
'access_level' => $accessLevel,
'caption' => $caption,
'created_by' => $userId,
'tags' => $tags,
'galleries' => $galleries,
];
if ($postId > 0) {
$options['post_id'] = $postId;
}
$result = $media->upload($file, $options);
if (!empty($result['success'])) {
$uploaded++;
} else {
$firstError = $firstError ?: ($result['error'] ?? 'upload_failed');
}
}
if ($uploaded > 0) {
$returnTo = $appendParam($returnTo, 'success', 'uploaded');
$returnTo = $appendParam($returnTo, 'count', (string)$uploaded);
if ($firstError) {
$returnTo = $appendParam($returnTo, 'warning', (string)$firstError);
}
redirect($returnTo);
}
redirect($appendParam($returnTo, 'error', $firstError ?: 'upload_failed'));
}
redirect($returnTo);
}
$attachPostId = (int)($_GET['attach_post_id'] ?? 0);
$returnTo = (string)($_GET['return_to'] ?? '');
$returnTo = (string)(parse_url($returnTo, PHP_URL_PATH) ?? '');
if ($returnTo === '' || strpos($returnTo, '/') !== 0 || strpos($returnTo, '/admin') !== 0) {
$returnTo = '/admin/media';
}
$editMediaId = (int)($_GET['edit_id'] ?? 0);
$editMedia = $editMediaId > 0 ? $media->getById($editMediaId) : null;
$editMediaTags = '';
$editMediaGalleries = '';
if ($editMedia) {
$editMediaTags = implode(', ', array_column($media->getMediaTerms($editMediaId, 'tag'), 'name'));
$editMediaGalleries = implode(', ', array_column($media->getMediaTerms($editMediaId, 'gallery'), 'name'));
}
$filters = [
'search' => trim((string)($_GET['q'] ?? '')),
'access_level' => isset($_GET['access_level']) ? (string)$_GET['access_level'] : '',
'unattached' => !empty($_GET['unattached']) ? 1 : 0,
];
$filterCombo = trim((string)($_GET['filter'] ?? ''));
if ($filterCombo !== '' && strpos($filterCombo, ':') !== false) {
list($taxSlug, $termSlug) = explode(':', $filterCombo, 2);
if (in_array($taxSlug, ['tag', 'gallery'], true) && $termSlug !== '') {
$filters['taxonomy_slug'] = $taxSlug;
$filters['term_slug'] = $termSlug;
}
}
$mediaItems = $media->getAllWithPostInfo($filters, 60, 0);
$mediaTotal = $media->countAll($filters);
$allPostsForAttach = $post->getAllPagesProjects() ?: [];
$allPostsList = $post->getAllPostsList() ?: [];
$allPostsForAttach = array_merge($allPostsForAttach, $allPostsList);
echo $template->render('admin/media', [
'page_title' => 'Media',
'layout_mode' => 'admin',
'success' => $_GET['success'] ?? null,
'warning' => $_GET['warning'] ?? null,
'error' => $_GET['error'] ?? null,
'filters' => $filters,
'filter_combo' => $filterCombo,
'attach_post_id' => $attachPostId,
'return_to' => $returnTo,
'edit_media' => $editMedia,
'edit_media_tags' => $editMediaTags,
'edit_media_galleries' => $editMediaGalleries,
'media_items' => $mediaItems,
'media_total' => $mediaTotal,
'media_tags' => $post->getTermsByTaxonomy('tag') ?: [],
'media_galleries' => $post->getTermsByTaxonomy('gallery') ?: [],
'media' => $media,
'all_posts_for_attach' => $allPostsForAttach,
'csrf_token' => $security->generateCSRFToken(),
]);
} elseif ($path === 'structure' || $path === 'admin/content') {
// Structure management: root pages/projects and child items
if (!$security->isLoggedIn()) {
redirect('/login');
}
if ($path === 'structure' && $_SERVER['REQUEST_METHOD'] === 'GET') {
redirect('/admin/content', 301);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$isAjax = strtolower((string)($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '')) === 'xmlhttprequest';
$respondJson = function($ok, $message = '', $httpCode = 200) {
http_response_code($httpCode);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'success' => (bool)$ok,
'message' => (string)$message,
]);
exit;
};
if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) {
if ($isAjax) {
$respondJson(false, 'Invalid security token.', 400);
}
redirect('/admin/content?error=invalid_token');
}
$structurePayload = trim((string)($_POST['structure_payload'] ?? ''));
if ($structurePayload !== '') {
$decodedPayload = json_decode($structurePayload, true);
if (!is_array($decodedPayload)) {
if ($isAjax) {
$respondJson(false, 'Invalid structure payload.', 400);
}
redirect('/admin/content?error=' . urlencode('Invalid structure payload.'));
}
if ($post->saveStructureTree($decodedPayload, $security->getCurrentUser()['id'])) {
if ($isAjax) {
$respondJson(true, 'Content tree updated.');
}
redirect('/admin/content?success=structure_updated');
}
if ($isAjax) {
$respondJson(false, $post->getLastError() ?: 'structure_update_failed', 400);
}
redirect('/admin/content?error=' . urlencode($post->getLastError() ?: 'structure_update_failed'));
}
$sortOrders = $_POST['sort_order'] ?? [];
if (is_array($sortOrders) && !empty($sortOrders)) {
foreach ($sortOrders as $postId => $value) {
$postId = (int)$postId;
if ($postId <= 0) {
continue;
}
$post->update($postId, ['sort_order' => (int)$value], $security->getCurrentUser()['id']);
}
}
if ($isAjax) {
$respondJson(false, 'Empty structure payload.', 400);
}
redirect('/admin/content?success=sort_updated');
}
$items = $post->getStructureTree();
$tree = buildTree($items ?: []);
$allPagesProjects = $post->getAllPagesProjects();
$allPosts = $post->getAllPostsList();
echo $template->render('admin/structure', [
'page_title' => 'Content',
'layout_mode' => 'admin',
'success' => $_GET['success'] ?? null,
'error' => $_GET['error'] ?? null,
'all_pages_projects' => $allPagesProjects,
'all_posts' => $allPosts,
'csrf_token' => $security->generateCSRFToken(),
'tree' => $tree,
]);
} elseif (
$path === 'new-post'
|| $path === 'admin/new'
|| preg_match('/^edit\/(.+)$/', $path, $matches)
|| preg_match('/^admin\/edit\/(.+)$/', $path, $matches)
) {
// Create/edit post
if (!$security->isLoggedIn()) {
redirect('/login');
}
if ($path === 'new-post' && $_SERVER['REQUEST_METHOD'] === 'GET') {
$queryString = $_SERVER['QUERY_STRING'] ?? '';
redirect('/admin/new' . ($queryString !== '' ? '?' . $queryString : ''), 301);
}
if (preg_match('/^edit\/(.+)$/', $path, $legacyEditMatches) && $_SERVER['REQUEST_METHOD'] === 'GET') {
redirect('/admin/edit/' . rawurlencode($legacyEditMatches[1]), 301);
}
$editSlug = isset($matches[1]) ? rawurldecode((string)$matches[1]) : null;
$editPost = null;
$isEditMode = false;
$formError = null;
if ($editSlug !== null && $editSlug !== '') {
$slugCandidates = array_values(array_unique(array_filter([
$editSlug,
trim($editSlug, '/'),
trim($editSlug, '/') !== '' ? trim($editSlug, '/') . '/' : null,
], function($value) {
return $value !== null && $value !== '';
})));
foreach ($slugCandidates as $slugCandidate) {
$editPost = $post->getBySlug($slugCandidate, 2);
if ($editPost) {
break;
}
}
if (!$editPost || $editPost['user_id'] != $security->getCurrentUser()['id']) {
redirect('/');
}
$isEditMode = true;
} elseif (!empty($_GET['parent_id'])) {
$parent = $post->getById((int)$_GET['parent_id']);
if ($parent && in_array(($parent['post_type'] ?? ''), ['page', 'project'], true)) {
$editPost = [
'parent_id' => (int)$parent['id'],
'post_type' => $parent['post_type'],
'access_level' => $parent['access_level'] ?? 0,
'status' => 'published',
];
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$formPath = $isEditMode && !empty($editPost['slug']) ? '/admin/edit/' . rawurlencode((string)$editPost['slug']) : '/admin/new';
// Check if it's a detach action (unassign media from post)
$action = $_POST['action'] ?? '';
if ($action === 'detach') {
$mediaId = (int)($_POST['media_id'] ?? 0);
$returnTo = $_POST['return_to'] ?? $formPath;
if ($mediaId > 0 && $media->detachFromPost($mediaId)) {
redirect($returnTo . '?success=detached');
}
redirect($returnTo . '?error=detach_failed');
}
if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) {
redirect($formPath . '?error=invalid_token');
}
$data = [
'title' => $_POST['title'] ?? '',
'slug' => $_POST['slug'] ?? '',
'parent_id' => $_POST['parent_id'] ?? null,
'template_key' => $_POST['template_key'] ?? null,
'menu_header' => !empty($_POST['menu_header']) ? 1 : 0,
'menu_footer' => !empty($_POST['menu_footer']) ? 1 : 0,
'menu_order' => (int)($_POST['menu_order'] ?? 0),
'meta_title' => $_POST['meta_title'] ?? null,
'meta_description' => $_POST['meta_description'] ?? null,
'excerpt' => $_POST['excerpt'] ?? null,
'meta_robots' => $_POST['meta_robots'] ?? 'index,follow',
'canonical_url' => $_POST['canonical_url'] ?? null,
'hreflang' => $_POST['hreflang'] ?? null,
'token_required' => !empty($_POST['token_required']) ? 1 : 0,
'access_token' => $_POST['access_token'] ?? null,
'sort_order' => (int)($_POST['sort_order'] ?? 0),
'content' => $_POST['content'] ?? '',
'access_level' => (int)($_POST['access_level'] ?? 0),
'post_type' => $_POST['post_type'] ?? 'post',
'status' => $_POST['status'] ?? 'published',
'categories' => [
'names' => $_POST['categories'] ?? '',
'selected_ids' => $_POST['category_ids'] ?? []
],
'tags' => [
'names' => $_POST['tags'] ?? '',
'selected_ids' => $_POST['tag_ids'] ?? []
],
'audience_segment_ids' => $_POST['audience_segment_ids'] ?? $_POST['guest_segment_ids'] ?? [],
'featured_media_id' => (int)($_POST['featured_media_id'] ?? 0) ?: null,
'og_media_id' => (int)($_POST['og_media_id'] ?? 0) ?: null,
];
$userId = $security->getCurrentUser()['id'];
if ($isEditMode && !empty($editPost['id'])) {
// Update
if ($post->update($editPost['id'], $data, $userId)) {
redirect($formPath . '?success=saved');
}
$formError = $post->getLastError() ?: 'Failed to update item.';
$editPost = array_merge($editPost ?? [], $data, ['id' => $editPost['id']]);
} else {
// Create
$postId = $post->create($data, $userId);
if ($postId) {
$newPost = $post->getById($postId);
redirect(buildPostRoutePath($newPost));
}
$formError = $post->getLastError() ?: 'Failed to create item.';
$editPost = array_merge($editPost ?? [], $data);
}
}
$attachedMedia = [];
if (!empty($editPost['id'])) {
$attachedMedia = $media->getByPost((int)$editPost['id']);
}
$libraryMedia = $media->getAll([], 500, 0);
echo $template->render('admin/post-edit', [
'page_title' => $isEditMode ? 'Edit Post' : 'New Post',
'layout_mode' => 'admin',
'success' => $_GET['success'] ?? null,
'error' => $formError,
'post' => $editPost,
'template_options' => getSelectableFrontendTemplates((string)($editPost['template_key'] ?? '')),
'pages' => $post->getAvailableParents($editPost['id'] ?? null),
'category_terms' => $post->getTermsByTaxonomy('category'),
'tag_terms' => $post->getTermsByTaxonomy('tag'),
'audience_segments' => $post->getAudienceSegments(),
'media' => $media,
'attached_media' => $attachedMedia,
'library_media' => $libraryMedia,
'csrf_token' => $security->generateCSRFToken()
]);
} elseif ($path === 'contact') {
$contactFlash = $_SESSION['contact_form_flash'] ?? null;
unset($_SESSION['contact_form_flash']);
$contactFormState = [
'status' => $contactFlash['status'] ?? null,
'message' => $contactFlash['message'] ?? null,
'values' => $contactFlash['values'] ?? ['name' => '', 'email' => '', 'company' => '', 'message' => '', 'source_topic' => '', 'source_url' => ''],
];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['contact_form'])) {
$name = trim((string)($_POST['name'] ?? ''));
$email = trim((string)($_POST['email'] ?? ''));
$company = trim((string)($_POST['company'] ?? ''));
$message = trim((string)($_POST['message'] ?? ''));
$website = trim((string)($_POST['website'] ?? ''));
$sourceTopic = trim((string)($_POST['source_topic'] ?? ''));
$sourceUrl = trim((string)($_POST['source_url'] ?? ''));
if ($sourceUrl === '') {
$sourceUrl = trim((string)($_SERVER['HTTP_REFERER'] ?? ''));
}
$flashValues = ['name' => $name, 'email' => $email, 'company' => $company, 'message' => $message, 'source_topic' => $sourceTopic, 'source_url' => $sourceUrl];
if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) {
$_SESSION['contact_form_flash'] = ['status' => 'error', 'message' => 'Invalid security token. Please refresh the page and try again.', 'values' => $flashValues];
redirect('/contact');
}
if ($website !== '') {
$_SESSION['contact_form_flash'] = ['status' => 'success', 'message' => 'Thank you. Your message has been sent.', 'values' => ['name' => '', 'email' => '', 'company' => '', 'message' => '', 'source_topic' => '', 'source_url' => '']];
redirect('/contact');
}
if ($name === '' || !$security->validateEmail($email) || $message === '') {
$_SESSION['contact_form_flash'] = ['status' => 'error', 'message' => 'Please fill in name, valid email, and message.', 'values' => $flashValues];
redirect('/contact');
}
$recipientEmail = trim((string)($config['install']['admin_email'] ?? ''));
$siteTitle = trim((string)($config['site']['title'] ?? 'NestCMS'));
if (!filter_var($recipientEmail, FILTER_VALIDATE_EMAIL)) {
$_SESSION['contact_form_flash'] = ['status' => 'error', 'message' => 'Contact form recipient email is not configured.', 'values' => $flashValues];
redirect('/contact');
}
$subjectTopicSafe = preg_replace("/[\r\n]+/", ' ', $sourceTopic);
$subject = $subjectTopicSafe !== '' ? 'New inquiry: ' . $subjectTopicSafe . ' — ' . $siteTitle : 'New inquiry — ' . $siteTitle;
$headers = implode("\r\n", [
'From: ' . $siteTitle . ' <' . $recipientEmail . '>',
'Reply-To: ' . $email,
'Content-Type: text/plain; charset=UTF-8',
]);
$body = "Name: {$name}\nEmail: {$email}\n";
if ($company !== '') {
$body .= "Company: {$company}\n";
}
if ($sourceTopic !== '') {
$body .= "Topic: {$sourceTopic}\n";
}
if ($sourceUrl !== '') {
$body .= "Source: {$sourceUrl}\n";
}
$body .= "\nMessage:\n{$message}\n";
$visitorMeta = $_SESSION['visitor_meta'] ?? [];
$visitorJourney = $_SESSION['visitor_journey'] ?? [];
$remoteAddr = trim((string)($_SERVER['REMOTE_ADDR'] ?? ''));
$forwardedFor = trim((string)($_SERVER['HTTP_X_FORWARDED_FOR'] ?? ''));
$userAgent = trim((string)($_SERVER['HTTP_USER_AGENT'] ?? ''));
$acceptLanguage = trim((string)($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''));
$dnt = trim((string)($_SERVER['HTTP_DNT'] ?? ''));
$cookies = array_keys($_COOKIE ?? []);
$body .= "\n---\nVisitor Context\n";
if ($remoteAddr !== '') {
$body .= "IP: {$remoteAddr}\n";
}
if ($forwardedFor !== '') {
$body .= "X-Forwarded-For: {$forwardedFor}\n";
}
if ($userAgent !== '') {
$body .= "User-Agent: {$userAgent}\n";
}
if ($acceptLanguage !== '') {
$body .= "Accept-Language: {$acceptLanguage}\n";
}
if ($dnt !== '') {
$body .= "DNT: {$dnt}\n";
}
$body .= "Session: " . session_id() . "\n";
if (is_array($visitorMeta) && !empty($visitorMeta)) {
foreach (['first_seen_at' => 'First Seen', 'first_seen_url' => 'First URL', 'first_referrer' => 'First Referrer', 'last_seen_at' => 'Last Seen', 'last_seen_url' => 'Last URL'] as $key => $label) {
$value = trim((string)($visitorMeta[$key] ?? ''));
if ($value !== '') {
$body .= $label . ": " . $value . "\n";
}
}
}
if (!empty($cookies)) {
$body .= "Cookies: " . implode(', ', $cookies) . "\n";
}
if (is_array($visitorJourney) && !empty($visitorJourney)) {
$body .= "\nJourney:\n";
$count = 0;
foreach ($visitorJourney as $item) {
if ($count >= 25) {
break;
}
$t = trim((string)($item['t'] ?? ''));
$u = trim((string)($item['u'] ?? ''));
if ($t !== '' && $u !== '') {
$body .= "- {$t} {$u}\n";
$count++;
}
}
}
$sent = @mail($recipientEmail, $subject, $body, $headers);
$_SESSION['contact_form_flash'] = $sent
? ['status' => 'success', 'message' => 'Thank you. Your message has been sent.', 'values' => ['name' => '', 'email' => '', 'company' => '', 'message' => '', 'source_topic' => '', 'source_url' => '']]
: ['status' => 'error', 'message' => 'Unable to send the message right now. Please try again later.', 'values' => $flashValues];
redirect('/contact');
}
$contactPage = null;
$contactPageId = (int)$db->getSetting('system_page_contact_id', '0');
if ($contactPageId > 0) {
$assignedContactPage = $post->getById($contactPageId);
$assignedContactPath = trim((string)($assignedContactPage['full_path'] ?? ''));
if ($assignedContactPage && ($assignedContactPage['post_type'] ?? '') === 'page' && ($assignedContactPage['status'] ?? '') === 'published' && $assignedContactPath !== '') {
$contactPage = $post->getByPath($assignedContactPath, $visibleAccessLevels, $audienceSegmentFilter);
}
}
if (!$contactPage) {
$contactPage = $post->getByPath('contact', $visibleAccessLevels, $audienceSegmentFilter);
}
if (!$renderPageResponse($contactPage, ['template_name' => 'contact', 'template_vars' => ['form_status' => $contactFormState['status'], 'form_message' => $contactFormState['message'], 'form_values' => $contactFormState['values']]])) {
$renderNotFoundResponse();
}
} elseif ($path === 'blog') {
$blogPage = null;
$blogPageId = (int)$db->getSetting('system_page_blog_index_id', '0');
if ($blogPageId > 0) {
$assignedBlogPage = $post->getById($blogPageId);
$assignedBlogPath = trim((string)($assignedBlogPage['full_path'] ?? ''));
if ($assignedBlogPage && ($assignedBlogPage['post_type'] ?? '') === 'page' && ($assignedBlogPage['status'] ?? '') === 'published' && $assignedBlogPath !== '') {
$blogPage = $post->getByPath($assignedBlogPath, $visibleAccessLevels, $audienceSegmentFilter);
}
}
if (!$blogPage) {
$blogPage = $post->getByPath('blog', $visibleAccessLevels, $audienceSegmentFilter);
}
$blogPosts = $post->getAll($visibleAccessLevels, 'post', 20, 0, $audienceSegmentFilter);
if ($blogPage && !canAccessTokenProtected($blogPage, $requestAccessToken, $security->isLoggedIn())) {
$renderNotFoundResponse();
} else {
echo $template->render('blog-index', [
'blog_page' => $blogPage,
'posts' => $blogPosts,
'meta_source' => $blogPage ?: [
'meta_title' => 'Blog',
'meta_description' => 'Latest posts',
],
'page_title' => $blogPage['title'] ?? 'Blog'
]);
}
} elseif (preg_match('#^blog/tag/([^/]+)$#', $path, $matches)) {
$termSlug = $matches[1];
$term = $post->getTermByTaxonomyAndSlug('tag', $termSlug);
if (!$term) {
$renderNotFoundResponse();
} else {
$archivePosts = $post->getPostsByTermSlug('tag', $termSlug, $visibleAccessLevels, 50, 0, $audienceSegmentFilter, 'post');
echo $template->render('blog-archive', [
'archive' => [
'name' => $term['name'],
'slug' => $term['slug'],
'description' => $term['description'] ?? '',
'taxonomy' => 'tag',
'full_path' => 'blog/tag/' . $term['slug'],
],
'meta_source' => [
'meta_title' => $term['name'],
'meta_description' => $term['description'] ?? null,
],
'posts' => $archivePosts,
'page_title' => 'Tag: ' . $term['name']
]);
}
} elseif (preg_match('#^blog/([^/]+)$#', $path, $matches)) {
$slug = $matches[1];
$term = $post->getTermByTaxonomyAndSlug('category', $slug);
if ($term) {
$archivePosts = $post->getPostsByTermSlug('category', $slug, $visibleAccessLevels, 50, 0, $audienceSegmentFilter, 'post');
echo $template->render('blog-archive', [
'archive' => [
'name' => $term['name'],
'slug' => $term['slug'],
'description' => $term['description'] ?? '',
'taxonomy' => 'category',
'full_path' => 'blog/' . $term['slug'],
],
'meta_source' => [
'meta_title' => $term['name'],
'meta_description' => $term['description'] ?? null,
],
'posts' => $archivePosts,
'page_title' => $term['name']
]);
} else {
$postData = $post->getBySlugAndType($slug, 'post', $visibleAccessLevels, $audienceSegmentFilter);
if (!$postData) {
$renderNotFoundResponse();
} elseif (!canAccessTokenProtected($postData, $requestAccessToken, $security->isLoggedIn())) {
$renderNotFoundResponse();
} else {
echo $template->render('blog-post', [
'post' => $postData,
'media' => $media,
'post_media' => !empty($postData['id']) ? $media->getByPost((int)$postData['id']) : [],
'page_title' => $postData['title'],
'csrf_token' => $security->generateCSRFToken()
]);
}
}
} elseif (preg_match('#^category/([^/]+)$#', $path, $matches)) {
redirect('/blog/' . rawurlencode($matches[1]), 301);
} elseif (preg_match('#^tag/([^/]+)$#', $path, $matches)) {
redirect('/blog/tag/' . rawurlencode($matches[1]), 301);
} elseif (preg_match('/^post\/(.+)$/', $path, $matches)) {
redirect('/blog/' . rawurlencode($matches[1]), 301);
} elseif (preg_match('/^(?:delete|admin\/delete)\/(.+)$/', $path, $matches)) {
// Delete post
if (!$security->isLoggedIn()) {
redirect('/login');
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo $template->render('404', [
'page_title' => 'Method Not Allowed'
]);
exit;
}
if (!$security->verifyCSRFToken($_POST['csrf_token'] ?? '')) {
redirect('/?error=invalid_token');
}
$slug = $matches[1];
$postData = $post->getBySlug($slug, 2);
if ($postData) {
$userId = $security->getCurrentUser()['id'];
$post->delete($postData['id'], $userId);
}
redirect('/');
} elseif (!empty($path)) {
// Universal hierarchy route: /parent/child/grandchild
$pageData = $post->getByPath($path, $visibleAccessLevels, $audienceSegmentFilter);
if (!$pageData) {
$renderNotFoundResponse();
} elseif (!canAccessTokenProtected($pageData, $requestAccessToken, $security->isLoggedIn())) {
$renderNotFoundResponse();
} else {
$children = $post->getChildrenByParent($pageData['id'], $visibleAccessLevels, 100, 0, $audienceSegmentFilter);
$hasChildren = !empty($children);
$templateName = resolveContentTemplateName($pageData, $hasChildren);
echo $template->render($templateName, [
'entry' => $pageData,
'page' => $pageData,
'post' => $pageData,
'section' => [
'name' => $pageData['title'],
'slug' => $pageData['slug'],
'description' => '',
'content' => $pageData['content'],
'full_path' => $pageData['full_path'] ?? '',
],
'meta_source' => $pageData,
'posts' => $children,
'children' => $children,
'has_children' => $hasChildren,
'media' => $media,
'post_media' => !empty($pageData['id']) ? $media->getByPost((int)$pageData['id']) : [],
'page_title' => $pageData['title'],
'csrf_token' => $security->generateCSRFToken()
]);
}
} else {
// 404
$renderNotFoundResponse();
}